#!/usr/bin/env python
# -*- coding: utf-8 -*-

#tested on python 2.6.2

"""
Usage: messagesV2.py [options]

Options:
  -h, --help      show this help message and exit
  -q              Don't output anything
  -b BODY         specify a text body (can be a text file)
  -s SUBJECT      specify a subject
  -c CONFIGFILE   specify a configuration file
  -l LOGFILE      specify a log file and activate logging.
  -a ATTACHMENTS  add an attachment (can be used more than one time)
"""

from re import search, I
from sys import argv, exit
from datetime import datetime
from os import getcwd, linesep
from os.path import join, isfile, basename

from smtplib import SMTP

from socket import socket, AF_INET, SOCK_STREAM

from email import Encoders
from email.Utils import formatdate
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
from email.MIMEMultipart import MIMEMultipart

from optparse import OptionParser
from ConfigParser import ConfigParser

class message:

    def __init__(self):
        self.attachments = []
        self.smtp_to = []
        """
        @param smtp_to: must be a list of addresses
        
        """
        self.security_systems = ['disabled', 'simple', 'tls']
        """
        @param message.security_system: list of available security parameters
        
            'disabled' : no authentication
            'simple' : username and password - no encryption
            'tls' : username and password - using SSL (like mail.google.com)
        
        """
        self.smtp_security = 'disabled'
        self.quiet = True # Don't output anything by default
        self.logFile = None # Output to command-line by default
        """ To enable logging set message.quiet to False
            and message.logFile to the path to a file """

    def unsetQuiet(self):
        self.quiet = False

    def setConfigFile(self, configFile):
        """
        Specify a configuration file
        
            @param configFile: Path to the configuration file
            -The configuration file must have a .ini-like design.
            -Parameters must be stored under a [mail] section.
            -Return specific error numbers if something is wrong.
        
        """
        if not isfile(configFile):
            if not isfile(join(getcwd(), configFile)):
                return(self._sendError(1001, configFile))
            self.configFile = join(getcwd(), configFile)
        self.configFile = configFile
        config = ConfigParser({'smtp_port': '25', 'smtp_from': 'max-k@post.com'})
        try:
            config.readfp(open(configFile))
        except:
            return(self._sendError(1002, configFile))
        for item in config.items('mail'):
            setattr(self, item[0], item[1])
        if hasattr(self, 'smtp_to'):
            self.smtp_to = self.smtp_to.split()
        return(0)

    def _verifyConfig(self):
        """
        Private : Verify if configuration is correct
        
            -Called by message.send() before sending an e-mail.
            -Return specific error numbers if something is wrong.
            -If not set, message.smtp_security will be set to
             'simple' if a username is specified.
            -If set to 'tls' or 'simple', it will be reset to
             'disabled' is no username is set.
        
        """
        if not hasattr(self, 'smtp_host'):
            return(self._sendError(1003))
        testHost = self.testHost(self.smtp_host, self.smtp_port)
        if testHost != 0:
            return(testHost)
        if not hasattr(self, 'smtp_to'):
            return(self._sendError(1004))
        testRecipient = self.testRecipient(self.smtp_to)
        if testRecipient != 0:
            return(testRecipient)
        if not hasattr(self, 'smtp_user'):
            self.smtp_security = 'disabled'
        else:
            if not hasattr(self, 'smtp_security'):
                self.smtp_security = 'simple'
            else:
                if self.smtp_security not in self.security_systems:
                    return(self._sendError(1005, self.smtp_security))
            if not hasattr(self, 'smtp_pass'):
                return(self._sendError(1006))
        return(0)

    def testHost(self, smtp_host, smtp_port='25'):
        """
        Test if port is open on smtp server
        
            -Called by message.verify() after checking if smtp_host is set.
            -Return specific error numbers if something is wrong.
        
        """
        test = socket(AF_INET,SOCK_STREAM)
        test.settimeout(3)
        try:
            testResult = test.connect_ex((smtp_host, int(smtp_port)))
        except:
            return(self._sendError(1007, smtp_host))
        if testResult == 11:
            testResult = 1008
        if testResult != 0:
            return(self._sendError(testResult, smtp_host, smtp_port))
        return(0)

    def testRecipient(self, smtp_to):
        """
        Test if syntaxe of recipient's e-mail address is correct
        
            -Called by message.verify() after checking if smtp_to is set.
            -Return error number 1009 if something is wrong.
        
        """
        for address in smtp_to:
            if not search('^([a-zA-Z0-9_-])+([.]?[a-zA-Z0-9_-]{1,})*@([a-zA-Z0-9-_]{2,}[.])+[a-zA-Z]{2,4}$', address, I):
                return(self._sendError(1009, address))
        return(0)

    def remove_attachment(self, file):
        """
        Add an attachment to the list message.attachments
        
            -Must be used before message.send() but after message().
            -Return error number 1010 is the file does not exists.
        
        """
        if not file in self.attachments:
            return(self._sendError(1010, file))
        self.attachments.remove(file)
        return(0)

    def add_attachment(self, file):
        """
        Remove an attachment from the list message.attachments
        
            -Must be used before message.send() but after message().
            -Return error number 1011 is the file does not exists.
        
        """
        if not isfile(file):
            return(self._sendError(1011, file))
        self.attachments.append(file)
        return(0)

    def _attachment(self, file):
        """
        Private : Add an attachment to the e-mail
        
            @warning: Cannot be undone.
            -Called by message.send() before sending an e-mail.
            -Return error number 1011 is the file does not exists.
        
        """
        if not isfile(file):
            return(self._sendError(1011, file))
        part = MIMEBase('application', 'octet-stream')
        part.set_payload(open(file,'r').read())
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s"' % basename(file))
        self.mail.attach(part)
        return(0)

    def checkEncoding(self, body):
        """
        Check the encoding of a text file and convert it to utf8
        
            -Support UTF-8 and ISO-8859-15 yet.
            -Return the converted file in a string
        
        """
        body_file = []
        with open(body) as bodyFile:
            for line in bodyFile:
                try:
                    line.decode('UTF-8')
                except:
                    line.decode('ISO-8859-15')
                body_file.append(line)
            return(''.join(body_file))

    def send(self, subject, body):
        """
        Build the e-mail and send it if possible
        
            -message.verifyconfig() is used to check the configuration
            before sending
            -message.attachement() is used to attach each files
            in message.attachements
            -Arg 'body' can be a string, the path to a text file
            or the path to an html file. (eg. /home/user/file.txt)
        
        """
        verifyConfig = self._verifyConfig()
        if verifyConfig != 0:
            return(verifyConfig)
        self.mail = MIMEMultipart()
        self.mail.set_charset('utf_8')
        self.mail['From'] = self.smtp_from
        self.mail['To'] = self.smtp_to
        self.mail['Date'] = formatdate(localtime=True)
        self.mail['Subject'] = subject
        if isfile(body):
            body = self.checkEncoding(body)
        if search('<html>', body, I):
            html_body = MIMEBase('text', 'html')
            html_body.set_payload(body)
            self.mail.attach(html_body)
        else:
            self.mail.attach(MIMEText(body))
        for file in self.attachments:
            self.last_added = file
            attachment = self._attachment(file)
            if attachment != 0:
                return(attachment)
        smtp = SMTP(self.smtp_host, self.smtp_port)
        if self.smtp_security != 'disabled':
            if self.smtp_security == 'tls':
                try:
                    smtp.set_debuglevel(1)
                    smtp.ehlo()
                    smtp.starttls()
                except:
                    return(self._sendError(1012, self.smtp_host))
            try:
                smtp.ehlo()
                smtp.login(self.smtp_user, self.smtp_pass)
            except:
                return(self._sendError(1013, self.smtp_host))
        try:
            smtp.sendmail(self.smtp_from, self.smtp_to, self.mail.as_string())
        except:
            return(self._sendError(999))
        smtp.quit()
        return(self._sendError(0))

    def getError(self, errorCode, errorValue1, errorValue2):
        """ Return the description of an error-code """
        errorCodes = {0: "Everything is OK, mail sent.",
        111: "Port " + errorValue1 + " is closed on " + errorValue2 + ".",
        113: "Host " + errorValue1 + " is unreachable.",
        999: "Unable to send this e-mail. Perhaps authentication is needed ?",
        1000: "Unidentified error. Perhaps implementation ?",
        1001: errorValue1 + " isn't a valid parameter file.",
        1002: "Unable to read config file" + errorValue1 + ". Syntaxe error.",
        1003: "Parameter smtp_host is not set.",
        1004: "Parameter smtp_to is not set.",
        1005: errorValue1 + " isn't a valid security parameter.",
        1006: "Password is defined, but not user name.",
        1007: "DNS error : Unable to resolve " + errorValue1 + ".",
        1008: "Host " + errorValue1 + " is not an smtp server.",
        1009: errorValue1 + " isn't a valid e-mail address.",
        1010: "Unable to remove attachment" + errorValue1 + ".",
        1011: "Unable to attach " + errorValue1 + ". It isn't a valid file.",
        1012: "StartTLS failed on " + errorValue1 + ".",
        1013: "Authentication failed on " + errorValue1 + ".",
        1014: "Unable to write to " + errorValue1 + ". Check right management.",
        }
        if errorCode not in errorCodes.keys():
            errorCode = 1000
        return(errorCodes.get(errorCode))

    def _sendError(self, errorCode, errorValue1='default', errorValue2='default'):
        """
        Private : Retrieve an error description from message.getError() an print it
        
            -Output nothing by default unless message.quiet is set to False
            -It can output to command-line interface or to message.logFile if set
        
        """
        errorMessage = self.getError(errorCode, errorValue1, errorValue2)
        if self.quiet == False:
            if self.logFile != None:
                log = self._log(errorMessage)
                if log != 0:
                    errorCode = log
                    print(self.getError(errorCode, self.logFile, errorValue2))
            else:
                print(errorMessage)
        return(errorCode)

    def _log(self, errorLog):
        """
        Private : Log date, time and the error returned by message.getError()
        
            -message._rotate() is used before logging
        
        """
        if self.logFile == basename(self.logFile):
            self.logFile = join(getcwd(), self.logFile)
        if isfile(self.logFile):
            self._rotate()
        try:
            logFile = open(self.logFile, 'a')
            logFile.write(str(datetime.now()) + ' : ' + errorLog + linesep)
            logFile.close()
            return(0)
        except:
            return(1014)

    def _rotate(self):
        """ Private : Clean a logfile to keep only 200 entries anytime """
        lines_buff = []
        with open(self.logFile, 'r') as logFile:
            for line in logFile:
                lines_buff.append(line)
        with open(self.logFile, 'w') as logFile:
            for element in lines_buff[0:199]:
                logFile.write(element)

if __name__ == '__main__':
    """
    OptionParser is used to parse command-line options.
    @see: http://docs.python.org/library/optparse.html
    
    """
    parser = OptionParser()
    parser.add_option("-q", action="store_true", dest="quiet", default=False, help="Don't output anything")
    parser.add_option("-b", dest="body", default="test_body", help="specify a text body (can be a text file)")
    parser.add_option("-s", dest="subject", default="test_subject", help="specify a subject")
    parser.add_option("-c", dest="configFile", help="specify a configuration file")
    parser.add_option("-l", dest="logFile", help="specify a log file and activate logging.")
    parser.add_option("-a", action="append", dest="attachments",
                      help="add an attachment (can be used more than one time)")
    (options, args) = parser.parse_args()
    msg=message()
    if options.quiet == False:
        msg.unsetQuiet()
    if options.configFile != None:
        if isfile(options.configFile) or isfile(join(getcwd(), options.configFile)):
            msg.setConfigFile(options.configFile)
        else:
            msg.setConfigFile(join(getcwd(), "params.ini"))
    msg.logFile = options.logFile
    if options.attachments != None:
        msg.attachments = options.attachments
    msg.send(options.subject, options.body)
    